/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service;

import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_CHANGE_COUNT;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_CHANGE_DURATION;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_END;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_END_LAT;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_END_LNG;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_END_SELECTION_TYPE;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_ID;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_IS_ARRIVAL_IND;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_SCHEDULE_ID;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_SCHEDULE_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_SCHEDULE_TRACK_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_SCOPE_FOLDER;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_SCOPE_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_START;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_START_LAT;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_START_LNG;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_START_SELECTION_TYPE;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_STOP_DIRECTION_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_STOP_ID;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_STOP_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_TRACK_ID;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_TRACK_NAME;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_TRANSPORT_ID;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_TRANSPORT_TYPES;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_TYPE;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_WALK_DISTANCE;
import static lt.marsrutai.mobile.android.service.BookmarkColumns.FIELD_WALK_SPEED;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lt.marsrutai.mobile.android.dao.Identifier;
import lt.marsrutai.mobile.android.model.Bookmark;
import lt.marsrutai.mobile.android.util.BeanUtils;
import lt.marsrutai.mobile.android.util.BeanUtils.BeanProperty;
import lt.marsrutai.mobile.android.util.ClassUtils;
import lt.marsrutai.mobile.android.util.StringUtils;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BookmarkService extends SQLiteOpenHelper {

    public static final int BOOKMARK_ADDED = 1;
    public static final int BOOKMARK_UPDATED = 2;
    public static final int BOOKMARK_NOT_ADDED = -1;
    public static final int BOOKMARK_NOT_UPDATED = -2;

    private static final String TABLE_NAME = "Bookmarks";
    private static final int DATABASE_VERSION = 4;

    /**
     * <p>
     * Column definitions.
     * </p>
     * <p>
     * Each top level array entry defines one field. The format of the definition is as follows:
     * </p>
     * <p>
     * <code>{"column name", "column type", "extra"}</code>.
     * <p>
     * <p>
     * E.g. <code>{"colName", "INT", "PRIMARY KEY"}</code>.
     * </p>
     */
    private static final String[][] COLUMNS = new String[][] {
            { FIELD_ID, ColumnTypes.INT, "PRIMARY KEY AUTOINCREMENT" },
            { FIELD_TYPE, ColumnTypes.TEXT }, 
            { FIELD_SCOPE_FOLDER, ColumnTypes.TEXT }, 
            { FIELD_SCOPE_NAME, ColumnTypes.TEXT }, 
            { FIELD_STOP_ID, ColumnTypes.TEXT },
            { FIELD_STOP_NAME, ColumnTypes.TEXT }, 
            { FIELD_STOP_DIRECTION_NAME, ColumnTypes.TEXT }, 
            { FIELD_TRANSPORT_ID, ColumnTypes.TEXT },
            { FIELD_SCHEDULE_ID, ColumnTypes.TEXT }, 
            { FIELD_SCHEDULE_NAME, ColumnTypes.TEXT }, 
            { FIELD_SCHEDULE_TRACK_NAME, ColumnTypes.TEXT },
            { FIELD_TRACK_ID, ColumnTypes.TEXT },
            { FIELD_TRACK_NAME, ColumnTypes.TEXT },
            // v2
            { FIELD_START, ColumnTypes.TEXT },
            { FIELD_END, ColumnTypes.TEXT },
            // v3
            { FIELD_NAME, ColumnTypes.TEXT },
            { FIELD_START_LAT, ColumnTypes.REAL },
            { FIELD_START_LNG, ColumnTypes.REAL },
            { FIELD_START_SELECTION_TYPE, ColumnTypes.TEXT },
            { FIELD_END_LAT, ColumnTypes.REAL },
            { FIELD_END_LNG, ColumnTypes.REAL },
            { FIELD_END_SELECTION_TYPE, ColumnTypes.TEXT },
            { FIELD_IS_ARRIVAL_IND, ColumnTypes.INT },
            // v4
            { FIELD_CHANGE_COUNT, ColumnTypes.INT },
            { FIELD_CHANGE_DURATION, ColumnTypes.INT },
            { FIELD_WALK_DISTANCE, ColumnTypes.INT },
            { FIELD_WALK_SPEED, ColumnTypes.INT },
            { FIELD_TRANSPORT_TYPES, ColumnTypes.TEXT }
    };

    public BookmarkService(Context context) {
        super(context, TABLE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(getCreateTableQuery());
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            addColumn(db, FIELD_START, ColumnTypes.TEXT);
            addColumn(db, FIELD_END, ColumnTypes.TEXT);
        }
        if (oldVersion < 3) {
            addColumn(db, FIELD_NAME, ColumnTypes.TEXT);
            addColumn(db, FIELD_START_LAT, ColumnTypes.REAL);
            addColumn(db, FIELD_START_LNG, ColumnTypes.REAL);
            addColumn(db, FIELD_START_SELECTION_TYPE, ColumnTypes.TEXT);
            addColumn(db, FIELD_END_LAT, ColumnTypes.REAL);
            addColumn(db, FIELD_END_LNG, ColumnTypes.REAL);
            addColumn(db, FIELD_END_SELECTION_TYPE, ColumnTypes.TEXT);
            addColumn(db, FIELD_IS_ARRIVAL_IND, ColumnTypes.INT);
        }
        if (oldVersion < 4) {
            addColumn(db, FIELD_CHANGE_COUNT, ColumnTypes.INT);
            addColumn(db, FIELD_CHANGE_DURATION, ColumnTypes.INT);
            addColumn(db, FIELD_WALK_DISTANCE, ColumnTypes.INT);
            addColumn(db, FIELD_WALK_SPEED, ColumnTypes.INT);
            addColumn(db, FIELD_TRANSPORT_TYPES, ColumnTypes.TEXT);
        }
    }

    protected void addColumn(SQLiteDatabase db, String columnName, String columnType) {
        db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + columnName + " " + columnType + ";");
    }

    /**
     * Builds CREATE TABLE query from column definitions ({@link #COLUMNS})
     * 
     * @return
     */
    protected String getCreateTableQuery() {
        List<String> fieldQueryParts = new ArrayList<String>();
        for (String[] fieldDefinition : COLUMNS) {
            fieldQueryParts.add(StringUtils.join(fieldDefinition, " "));
        }

        String fieldsQuery = StringUtils.join(fieldQueryParts, ", ");

        return String.format("CREATE TABLE %s (%s);", TABLE_NAME, fieldsQuery);
    }

    /**
     * Adds a bookmark.
     * 
     * @param bookmark
     * @return <code>true</code> is bookmark was added, <code>false</code> otherwise
     */
    public boolean addBookmark(Bookmark bookmark) {
        // Gets the data repository in write mode
        SQLiteDatabase db = getWritableDatabase();

        ContentValues values = getContentValues(bookmark);

        long rowId = db.insert(TABLE_NAME, null, values);

        if (rowId > -1) {
            bookmark.setId(rowId);
        }

        return rowId > -1;
    }

    public boolean removeBookmark(Bookmark bookmark) {
        return delete(bookmark.getId() == null ? findBookmark(bookmark) : bookmark);
    }

    private boolean delete(Bookmark bookmark) {
        SQLiteDatabase db = getWritableDatabase();

        return db.delete(TABLE_NAME, FIELD_ID + "=?", new String[] { Long.toString(bookmark.getId()) }) == 1;
    }

    public boolean updateBookmark(Bookmark bookmark) {
        return update(bookmark.getId() == null ? findBookmark(bookmark) : bookmark);
    }

    private boolean update(Bookmark bookmark) {
        SQLiteDatabase db = getWritableDatabase();

        return db.update(TABLE_NAME, getContentValues(bookmark), FIELD_ID + "=?", new String[] { Long.toString(bookmark.getId()) }) == 1;
    }

    public int updateOrAddBookmark(Bookmark bookmark) {
        Bookmark savedBookmark = bookmark.getId() == null ? findBookmark(bookmark) : bookmark;
        
        if (savedBookmark == null) {
            return addBookmark(bookmark) ? BOOKMARK_ADDED : BOOKMARK_NOT_ADDED;
        }
        return update(savedBookmark) ? BOOKMARK_UPDATED : BOOKMARK_NOT_UPDATED;
    }

    protected ContentValues getContentValues(Bookmark bookmark) {
        Set<String> columnNames = new HashSet<String>();

        for (String[] fieldDefinition : COLUMNS) {
            columnNames.add(fieldDefinition[0]);
        }

        // Create a new map of values, where column names are the keys
        ContentValues values = new ContentValues();

        values.put(FIELD_TYPE, bookmark.getClass().getSimpleName());
        for (BeanProperty property : BeanUtils.getProperties(bookmark.getClass())) {
            if (columnNames.contains(property.getName())) {
                storeProperty(values, bookmark, property);
            }
        }
        return values;
    }

    public <T extends Bookmark> T findBookmark(T bookmark) {
        SQLiteDatabase db = getReadableDatabase();

        @SuppressWarnings("unchecked")
        Class<T> bookmarkClass = (Class<T>) bookmark.getClass();

        List<BeanProperty> propertiesToSelect = getPropertiesToSelect(bookmarkClass);
        String[] columnNames = new String[propertiesToSelect.size()];
        for (int i = 0; i < propertiesToSelect.size(); i++) {
            columnNames[i] = propertiesToSelect.get(i).getName();
        }

        String[] selectableFields = getSelectableFields(propertiesToSelect);

        String[] arguments = formArgs(bookmark, propertiesToSelect, selectableFields);

        Cursor cursor = db.query(TABLE_NAME, columnNames, formSelection(selectableFields, arguments), clearNulls(arguments), null, null,
                FIELD_ID + " DESC");
        List<T> bookmarks = getResults(cursor, bookmarkClass, propertiesToSelect);
        
        if (bookmarks.isEmpty()) {
            return null;
        }
        
        return bookmarks.get(0);
    }

    private String[] clearNulls(String[] arguments) {
        List<String> values = new ArrayList<String>();

        for (String argument : arguments) {
            if (argument != null) {
                values.add(argument);
            }
        }
        return values.toArray(new String[values.size()]);
    }

    private String[] formArgs(Object object, List<BeanProperty> propertiesToSelect, String[] selectableFields) {
        List<String> selectableFieldList = Arrays.asList(selectableFields);
        List<String> result = new ArrayList<String>();

        for (BeanProperty property : propertiesToSelect) {
            if (selectableFieldList.contains(property.getName())) {
                result.add(toString(object, property));
            }
        }
        return result.toArray(new String[result.size()]);
    }

    private String[] getSelectableFields(List<BeanProperty> propertiesToSelect) {
        List<String> result = new ArrayList<String>();

        for (BeanProperty property : propertiesToSelect) {
            if (property.getGetter().isAnnotationPresent(Identifier.class)) {
                result.add(property.getName());
            }
        }

        return result.toArray(new String[result.size()]);
    }

    private String formSelection(String[] columns, String[] arguments) {
        String param = "=?";

        if (columns.length == 0) {
            return null;
        }

        List<String> parts = new ArrayList<String>();
        for (int i = 0; i < arguments.length; i++) {
            parts.add(columns[i] + (arguments[i] == null ? " IS NULL " : param));
        }
        return StringUtils.join(parts, " AND ");
    }

    private String[] formArgs(String... args) {
        String[] result = new String[args.length];

        for (int i = 0; i < args.length; i++) {
            result[i] = args[i];
        }
        return result;
    }

    public <T extends Bookmark> boolean containsBookmark(T bookmark) {
        return findBookmark(bookmark) != null;
    }

    public <T extends Bookmark> List<T> getBookmarks(Class<T> bookmarkClass, String sortColumn) {
        SQLiteDatabase db = getReadableDatabase();

        List<BeanProperty> propertiesToSelect = getPropertiesToSelect(bookmarkClass);
        String[] columnNames = new String[propertiesToSelect.size()];
        for (int i = 0; i < propertiesToSelect.size(); i++) {
            columnNames[i] = propertiesToSelect.get(i).getName();
        }

        Cursor cursor = db.query(TABLE_NAME, columnNames, FIELD_TYPE + "=?", formArgs(bookmarkClass.getSimpleName()), null, null,
                sortColumn == null ? null : sortColumn + " COLLATE NOCASE");
        List<T> bookmarks = getResults(cursor, bookmarkClass, propertiesToSelect);

        return bookmarks;
    }

    protected <T> List<T> getResults(Cursor cursor, Class<T> bookmarkClass, List<BeanProperty> propertiesToSelect) {
        List<T> bookmarks = new ArrayList<T>();

        try {
            while (cursor.moveToNext()) {
                T bookmark = ClassUtils.instantiateClass(bookmarkClass);

                // fill object with data
                for (BeanProperty beanProperty : propertiesToSelect) {
                    BeanUtils.setPropertyValue(bookmark, beanProperty, retrieveProperty(cursor, beanProperty));
                }

                bookmarks.add(bookmark);
            }
        }
        finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return bookmarks;
    }

    private List<BeanProperty> getPropertiesToSelect(Class<?> bookmarkClass) {
        Set<String> columnNames = new HashSet<String>();

        for (String[] fieldDefinition : COLUMNS) {
            columnNames.add(fieldDefinition[0]);
        }

        List<BeanProperty> propertiesToSelect = new ArrayList<BeanProperty>();
        for (BeanProperty property : BeanUtils.getProperties(bookmarkClass)) {
            if (columnNames.contains(property.getName())) {
                propertiesToSelect.add(property);
            }
        }

        return propertiesToSelect;
    }

    protected void storeProperty(ContentValues values, Bookmark bookmark, BeanProperty property) {
        Object value = BeanUtils.getPropertyValue(bookmark, property);
        Class<? extends Object> propertyType = property.getType();

        if (byte.class.isAssignableFrom(propertyType) || Byte.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Byte) value);
        }
        else if (short.class.isAssignableFrom(propertyType) || Short.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Short) value);
        }
        else if (int.class.isAssignableFrom(propertyType) || Integer.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Integer) value);
        }
        else if (long.class.isAssignableFrom(propertyType) || Long.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Long) value);
        }
        else if (float.class.isAssignableFrom(propertyType) || Float.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Float) value);
        }
        else if (double.class.isAssignableFrom(propertyType) || Double.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Double) value);
        }
        else if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Boolean) value);
        }
        else if (char.class.isAssignableFrom(propertyType) || Character.class.isAssignableFrom(propertyType)
                || String.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), value == null ? null : value.toString());
        }
        else if (propertyType.isEnum()) {
            values.put(property.getName(), value == null ? null : ((Enum<?>) value).name());
        }
        else if (Collection.class.isAssignableFrom(propertyType)) {
            // Support for String collections
            values.put(property.getName(), value == null ? null : StringUtils.join((Collection<?>) value, ","));
        }
        else {
            throw new IllegalArgumentException(String.format("Unsupported property type %s.", propertyType));
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected Object retrieveProperty(Cursor cursor, BeanProperty beanProperty) {
        int columnIndex = cursor.getColumnIndex(beanProperty.getName());
        Class<?> propertyType = beanProperty.getType();

        if (cursor.isNull(columnIndex)) {
            return null;
        }
        else if (byte.class.isAssignableFrom(propertyType) || Byte.class.isAssignableFrom(propertyType)) {
            return new Byte((byte) cursor.getInt(columnIndex));
        }
        else if (short.class.isAssignableFrom(propertyType) || Short.class.isAssignableFrom(propertyType)) {
            return cursor.getShort(columnIndex);
        }
        else if (int.class.isAssignableFrom(propertyType) || Integer.class.isAssignableFrom(propertyType)) {
            return cursor.getInt(columnIndex);
        }
        else if (long.class.isAssignableFrom(propertyType) || Long.class.isAssignableFrom(propertyType)) {
            return cursor.getLong(columnIndex);
        }
        else if (float.class.isAssignableFrom(propertyType) || Float.class.isAssignableFrom(propertyType)) {
            return cursor.getFloat(columnIndex);
        }
        else if (double.class.isAssignableFrom(propertyType) || Double.class.isAssignableFrom(propertyType)) {
            return cursor.getDouble(columnIndex);
        }
        else if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            return Boolean.valueOf(cursor.getInt(columnIndex) == 1);
        }
        else if (char.class.isAssignableFrom(propertyType) || Character.class.isAssignableFrom(propertyType)) {
            return cursor.getString(columnIndex).charAt(0);
        }
        else if (String.class.isAssignableFrom(propertyType)) {
            return cursor.getString(columnIndex);
        }
        else if (propertyType.isEnum()) {
            return Enum.valueOf((Class) propertyType, cursor.getString(columnIndex));
        }
        else if (Collection.class.isAssignableFrom(propertyType)) {
            String value = cursor.getString(columnIndex);
            return value == null ? null : Arrays.asList(value.split(","));
        }
        else {
            throw new IllegalArgumentException(String.format("Unsupported property type %s.", propertyType));
        }
    }

    protected String toString(Object object, BeanProperty property) {
        Object value = BeanUtils.getPropertyValue(object, property);
        Class<? extends Object> propertyType = property.getType();

        if (value == null) {
            return null;
        }
        else if (byte.class.isAssignableFrom(propertyType) || Byte.class.isAssignableFrom(propertyType)) {
            return Byte.toString((Byte) value);
        }
        else if (short.class.isAssignableFrom(propertyType) || Short.class.isAssignableFrom(propertyType)) {
            return String.valueOf((Short) value);
        }
        else if (int.class.isAssignableFrom(propertyType) || Integer.class.isAssignableFrom(propertyType)) {
            return String.valueOf((Integer) value);
        }
        else if (long.class.isAssignableFrom(propertyType) || Long.class.isAssignableFrom(propertyType)) {
            return String.valueOf((Long) value);
        }
        else if (float.class.isAssignableFrom(propertyType) || Float.class.isAssignableFrom(propertyType)) {
            return String.valueOf((Float) value);
        }
        else if (double.class.isAssignableFrom(propertyType) || Double.class.isAssignableFrom(propertyType)) {
            return String.valueOf((Double) value);
        }
        else if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            return ((Boolean) value) ? "1" : "0";
        }
        else if (char.class.isAssignableFrom(propertyType) || Character.class.isAssignableFrom(propertyType)
                || String.class.isAssignableFrom(propertyType)) {
            return value.toString();
        }
        else if (propertyType.isEnum()) {
            return ((Enum<?>) value).name();
        }
        else if (Collection.class.isAssignableFrom(propertyType)) {
            // Support for String collections
            return StringUtils.join((Collection<?>) value, ",");
        }
        else {
            throw new IllegalArgumentException(String.format("Unsupported property type %s.", propertyType));
        }
    }
}
